/*
 * Copyright 2011 Mark Davidson.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.dctmcommons.impl;

import java.util.ArrayList;
import java.util.List;

import org.dctmcommons.IMethodBuilder;

import com.documentum.com.DfClientX;
import com.documentum.com.IDfClientX;
import com.documentum.fc.client.IDfCollection;
import com.documentum.fc.client.IDfSession;
import com.documentum.fc.common.DfException;
import com.documentum.fc.common.IDfList;

import static com.documentum.fc.methodserver.DfStandardJobArgumentNames.*;

/**
 * Default implementation of {@link IMethodBuilder}. Example usage:
 * 
 * <pre>
 * final DctmCommons dc = DctmCommon.getInstance();
 * final int ret = dc.withMethod("mymethod")
 *                   .useSessionCredentials()
 *                   .setTraceLevel(5)
 *                   //.runAsynchronously()
 *                   .add("custom_string_param", "value")
 *                   .add("custom_int_param", 7)
 *                   .execute(session);
 * </pre>
 * 
 * <p>
 * The above example demonstrates how to run a server method called "mymethod"
 * synchronously (the default mode) at trace level 5, explicitly passing
 * custom parameters "custom_string_param" and "custom_int_param". The username,
 * session ticket and repository name are inferred from the session that is
 * passed to the execute() method. Session credentials can be explicitly
 * specified by replacing the useSessionCredentials() call with calls to
 * setUsername(), setTicket() and setRepositoryName().
 * </p>
 * 
 * @author Mark Davidson
 */
public class MethodBuilder implements IMethodBuilder {

	/** Single quote character constant. */
	private static final String QUOTE = "'";
	
	/** Double quote character constant. */
	private static final String DQUOTE = "\"";
	
	/** Dash character constant. */
	private static final String DASH = "-";

	/** Space character constant. */
	private static final String SPACE = " ";

	/** Method arg key - domain. */
	private static final String KEY_DOMAIN = "domain";
	
	/** Method arg key - ticket. */
	private static final String KEY_TICKET = "ticket";
	
	/** The data types. */
	private IDfList dataTypes = null;
	
	/** The method arguments names. */
	private IDfList methodArgNames = null;
	
	/** The method parameter values. */
	private IDfList paramValues = null;
	
	/** The method arguments. */
	private List<Argument> args = null;
	
	/** The DFC facade. */
	private IDfClientX clientx = null;
	
	/** Whether to run the server method asynchronously. */
	private boolean async = false;
	
	/** Whether to save the results of the method invocation. */
	private boolean saveResults = false;
	
	/** The username. */
	private String username = null;
	
	/** The domain. */
	private String domain = null;
	
	/** The password or login ticket. */
	private String ticket = null;
	
	/** The repository name. */
	private String repositoryName = null;
	
	/** The method trace level. */
	private int traceLevel = 0;
	
	/** Specifies whether the session credentials should be used. */
	private boolean useSessionCredentials = false;
	
	/** The name of the method to be invoked. */
	private String methodName = null;
	
	/** Method param data type - boolean. */
	public static final String DATA_TYPE_BOOLEAN = "B";
	
	/** Method param data type - integer. */
	public static final String DATA_TYPE_INTEGER = "I";
	
	/** Method param data type - string. */
	public static final String DATA_TYPE_STRING = "S";
	
	/** String param value - true. */
	public static final String TRUE = "T";
	
	/** String param value - false. */
	public static final String FALSE = "F";
	
	/**
	 * Default constructor.
	 * @param methodName The method name
	 * @throws DfException Repository exception
	 */
	private MethodBuilder(final String methodName) {
		clientx = new DfClientX();
		args = new ArrayList<Argument>();
		this.methodName = methodName;
	}
	
	/**
	 * Factory method.
	 * @param methodName The method name
	 * @return The IMethodBuilder instance
	 */
	public static IMethodBuilder withMethod(final String methodName) {
		return new MethodBuilder(methodName);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#useSessionCredentials()
	 */
	@Override
	public IMethodBuilder useSessionCredentials() {
		this.useSessionCredentials = true;
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#saveResults()
	 */
	@Override
	public IMethodBuilder saveResults() {
		this.saveResults = true;
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#runAsynchronously()
	 */
	@Override
	public IMethodBuilder runAsynchronously() {
		this.async = true;
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#addRepository(java.lang.String)
	 */
	@Override
	public IMethodBuilder addRepository(final String repositoryName) {
		this.repositoryName = repositoryName;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#addUsername(java.lang.String)
	 */
	@Override
	public IMethodBuilder addUsername(final String username) {
		this.username = username;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#addDomain(java.lang.String)
	 */
	@Override
	public IMethodBuilder addDomain(final String domain) {
		this.domain = domain;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#addTicket(java.lang.String)
	 */
	@Override
	public IMethodBuilder addTicket(final String ticket) {
		this.ticket = ticket;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#setTraceLevel(int)
	 */
	@Override
	public IMethodBuilder setTraceLevel(final int traceLevel) {
		this.traceLevel = traceLevel;
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, java.lang.String)
	 */
	@Override
	public IMethodBuilder add(final String key, final String value) {
		final Argument arg = new Argument();
		arg.isMultivalued = false;
		arg.key = key;
		arg.value = value;
		args.add(arg);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, boolean)
	 */
	@Override
	public IMethodBuilder add(final String key, final boolean value) {
		final Argument arg = new Argument();
		arg.isMultivalued = false;
		arg.key = key;
		arg.value = value;
		args.add(arg);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, int)
	 */
	@Override
	public IMethodBuilder add(final String key, final int value) {
		final Argument arg = new Argument();
		arg.isMultivalued = false;
		arg.key = key;
		arg.value = value;
		args.add(arg);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, java.lang.String[])
	 */
	@Override
	public IMethodBuilder add(final String key, final String[] value) {
		final Argument arg = new Argument();
		arg.isMultivalued = true;
		arg.key = key;
		arg.value = value;
		args.add(arg);
		return this;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, boolean[])
	 */
	@Override
	public IMethodBuilder add(final String key, final boolean[] value) {
		final Argument arg = new Argument();
		arg.isMultivalued = true;
		arg.key = key;
		
		if (value != null && value.length > 0) {
			final String[] strValues = new String[value.length];
			for (int i = 0; i < value.length; i++) {
				strValues[i] = String.valueOf(value[i]);
			}
			arg.value = strValues;
		}
		
		args.add(arg);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#add(java.lang.String, int[])
	 */
	@Override
	public IMethodBuilder add(final String key, final int[] value) {
		final Argument arg = new Argument();
		arg.isMultivalued = true;
		arg.key = key;
		
		if (value != null && value.length > 0) {
			final String[] strValues = new String[value.length];
			for (int i = 0; i < value.length; i++) {
				strValues[i] = String.valueOf(value[i]);
			}
			arg.value = strValues;
		}
		
		args.add(arg);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * @see org.dctmcommons.IMethodBuilder#build(com.documentum.fc.client.IDfSession)
	 */
	@Override
	public int execute(final IDfSession session) throws DfException {
		methodArgNames = clientx.getList();
		dataTypes = clientx.getList();
		paramValues = clientx.getList();
		
		IDfCollection results = null;
		int returnState = 0;
		
		if (useSessionCredentials) {
			inferSessionCredentialsDetails(session);
		}
		
		methodArgNames.appendString("METHOD");
		methodArgNames.appendString("TRACE_LAUNCH");
		methodArgNames.appendString("SAVE_RESULTS");
		methodArgNames.appendString("LAUNCH_ASYNC");
		methodArgNames.appendString("ARGUMENTS");
		
		dataTypes.appendString(DATA_TYPE_STRING);
		dataTypes.appendString(DATA_TYPE_BOOLEAN);
		dataTypes.appendString(DATA_TYPE_STRING);
		dataTypes.appendString(DATA_TYPE_BOOLEAN);
		dataTypes.appendString(DATA_TYPE_STRING);
		
		paramValues.appendString(methodName);
		paramValues.appendString(TRUE);
		paramValues.appendString(saveResults ? TRUE : FALSE);
		paramValues.appendString(async ? TRUE : FALSE);		
		paramValues.appendString(buildArguments());
		
		try {
			results = session.apply(null,
									"DO_METHOD",
									methodArgNames,
									dataTypes,
									paramValues);
			
			if (results != null && results.next()) {
				returnState = results.getInt("method_return_val");
			}
		} finally {
			if (results != null) {
				closeCollection(results);
			}
		}
		
		return returnState;
	}
	
	/**
	 * Closes a collection
	 * @param collection The collection
	 * @throws DfException The repository exception
	 */
    private void closeCollection(IDfCollection collection) throws DfException {
    	if (collection != null) {
    		collection.close();
    	}
    }

	
	/**
	 * Joins the contents of a string array.
	 * @param values The string array
	 * @param delimiter The delimiter
	 * @return The resulting string value
	 */
	private String join(final String[] values, String delimiter) {
		if (values != null) {
			final int length = values.length;
			if (length == 0) {
				return null;
			} else {
				final StringBuilder sb = new StringBuilder();
				for (int i = 0; i < length; i++) {
					sb.append(values[i] + ",");
				}
				sb.deleteCharAt(length - 1);
				return sb.toString();
			}
		} else {
			return null;
		}
	}
	
	/**
	 * Builds the method arguments string.
	 * @return The arguments
	 */
	private String buildArguments() {
		final StringBuilder sb = new StringBuilder();
		
		if (repositoryName != null) {
			sb.append(DASH + DOCBASE_NAME + SPACE + repositoryName + SPACE);
		}
		
		if (username != null) {
			sb.append(DASH + USER_NAME + SPACE + username + SPACE);
		}
		
		if (ticket != null) {
			sb.append(DASH + KEY_TICKET + SPACE + ticket + SPACE);
		}

		if (domain != null) {
			sb.append(DASH + KEY_DOMAIN + SPACE + domain + SPACE);
		}
		
		sb.append(DASH + METHOD_TRACE_LEVEL + SPACE + traceLevel + SPACE);
		
		// add non-standard arguments
		if (args != null && args.size() > 0) {
			for (final Argument arg : args) {
				sb.append(DASH + arg.key + SPACE);
				if (arg.isMultivalued) {
					if (arg.value != null) {
						sb.append(DQUOTE + join((String[])arg.value, ",") + DQUOTE);
					}
				} else {
					sb.append(DQUOTE + arg.value + DQUOTE);
				}
			}
			sb.append(SPACE);
		}

		// chop off trailing space then wrap in single quotes
		sb.deleteCharAt(sb.length() - 1).insert(0, QUOTE).append(QUOTE);
		System.out.println(sb);
		return sb.toString();
	}

	/**
	 * Infers the session credentials details.
	 * @throws DfException The repository exception 
	 */
	private void inferSessionCredentialsDetails(final IDfSession session)
		throws DfException {
		this.username = session.getLoginUserName();
		this.ticket = session.getLoginTicket();
		this.repositoryName = session.getDocbaseName();
	}
	
	/**
	 * Encapsulates method arguments.
	 */
	private class Argument {
		public boolean isMultivalued = false;
		public String key = null;
		public Object value = null;
	}
}
